#include <iostream>
#include <sys/select.h>
#include "TcpServer.hpp"
#include <vector>

using namespace std;

#define FDS_SIZE (sizeof(fd_set) * 8)
#define MTY -1

// 保存历史合法数据
int fdsarray[FDS_SIZE] = {0};
int gnum = sizeof(fdsarray) / sizeof(fdsarray[0]);

// 并且 select 就绪之后, HandleEvent 需要处理完所有的 readfds
static void HandleEvent(int lisSock, fd_set& readfds)
{
    for (int k = 0; k < gnum; k ++)
    {
        // 如果这个无效, 那么跳过
        if (fdsarray[k] == MTY)     continue; 
        // 还需要判断是不是监听套接字   
        if (k == 0 && fdsarray[k] == lisSock) 
        {

            // 如何得知 哪些 fd 上面的事件就绪了
            // 这个 lisSock 在 readfds 集合中, 就证明已经有了一个新连接
            if (FD_ISSET(lisSock, &readfds))
            {
                cout << "成功获取一个新连接" << endl;
            }
            // listenSock 这个监听的文件描述符捕捉到了读事件, 说明有连接达成了
            // 可以进行 accept 了
            string cltIp;
            uint16_t cltPort = 0;
            // 这里在接收的时候是否会阻塞? 不会被阻塞，因为明确知道了 监听的文件描述符是发生了读事件才告知用户的
            int sock = Sock::Accept(lisSock, &cltIp, &cltPort);
            if (sock < 0)
                return ;
            cout << "获取新连接成功" << endl;
            // 然后这时候不能马上进行读写操作, 因为这只是连接建立好了, 不代表有人往里面写入数据了
            // 再将 sock 交付给 select 
            int i ;
            for (i = 0; i < gnum; i ++)
            {
                if (fdsarray[i] = MTY)  break;
            }
            if (i == gnum) 
            {
                cerr << "服务器负载上限" << endl;
                close(sock);
                return ;
            }
            else            
            {
                // 将当前的服务 sock 交付给 select 进行管理
                // 然后当 select 接收到读事件或者写事件的时候就会告知用户
                fdsarray[i] = sock; 
            }
        }
        else 
        {
            // 处理 server_sock 的 IO 事件
            if (FD_ISSET(fdsarray[k], &readfds))
            {
                // 合法的 IO 事件 sock 就绪
                // recv/read 进行读取
                char buffer[1024];
                ssize_t rd = recv(fdsarray[k], buffer, sizeof(buffer) - 1, 0); // 不会阻塞
                if (rd > 0)
                {
                    buffer[rd] = '\0';
                    cout << "客户端发送消息 [文件下标是：" << fdsarray[k] << "] ：" << buffer << endl;
                }
                else if (rd == 0)
                {
                    // 写端不写了
                    cout << "客户端关闭连接 [文件下标是：" << fdsarray[k] << "] ： 拜拜" << endl;
                    close(fdsarray[k]); // 连接完成，关闭监听
                    fdsarray[k] = MTY;
                }
            }
        }
    }
}

int main(int argc, char* argv[])
{
    fd_set fds; // fd_set 其实就是表示 fd 的位图
    // 最多只能放1024个文件描述符
    // 获取监听的文件描述符
    int lisSock = Sock::GetSock();
    int port = atoi(argv[1]);
    Sock::Bind(lisSock, port);
    Sock::Listen(lisSock);  // 开始监听

    // fdsarray 的初始化
    for (int i = 0; i < gnum; i ++)     fdsarray[i] = MTY; // 表示没有被使用
    fdsarray[0] = lisSock; // 0 号下标默认保存监听套接字

    while (true)
    {
        // 每次选择的时候, readfds 集都需要重新设定
        int maxfd = MTY;
        fd_set readfds;
        FD_ZERO(&readfds);
        for (int i = 0; i < gnum; i ++)
        {
            if (fdsarray[i] == MTY) continue; // 过滤不合法的 fd
            FD_SET(fdsarray[i], &readfds);    // 添加所有合法的 fd 到 readfds 中，方便 select 进行统一管理
            if (maxfd < fdsarray[i])          // 更新最大值
                maxfd = fdsarray[i];
        }
        // 捕捉 监听sock 有没有读事件发生

        struct timeval timeout = {105, 0};
        // 如何看待监听 Socket, 获取新连接的，本质就先需要3次握手,前提是先发送syn
        // 实际上建立连接的本质，也就是 IO，一个建立好的连接，就称为 —— 读事件就绪
        // lisSock 要关心的也就只是读事件就绪
        // accept 其实也就是 等 + 数据拷贝
        // int sock = Sock::Accept(lisSock, );

        // ***************
        // 编写多路转接代码的时候，必须先保证条件就绪了，才能调用 IO 类函数
        // os 需要关注的文件描述符，并且在就绪的时候告知用户
        int n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
        switch(n)
        {
            case 0:
                cout << "等待超时" << endl;
                break;
            case -1:
                cerr << errno << " = " << strerror(errno) << endl;
                break;
            default:
                // 处理 readfds 集中所有事件
                HandleEvent(lisSock, readfds);
                // 否则等待成功
                // 1. 服务器刚启动的时候，只有一个 fd -- listen_sock
                // 2. server 运行的时候, sock 才会慢慢增多
                // 3. select 使用位图, 采用输入输出型参数的方式, 来进行 内核和用户 之间信息的传递
                // 4. select 的每一次调用，都需要对历史数据和 sock 进行重新设置
                // 5. 因此 select 就绪的时候, 可能是 listen 文件接收到了读事件, 也可能是 server socket 读到了 IO 事件
                break;
        }
    }
    return 0;
}